package border;

import square.Direction;
import square.Square;
import square.Rock;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * A class for constructing borders, attached to maximum 2 squares.
 * 
 * @invar 	Each border must have 1 or 2 proper squares attached to it.
 * 			| hasProperSquares()
 * 
 * @version	3.0
 * @author 	Inge Gielis <inge.gielis@student.kuleuven.be>
 * @author 	Annelies Ketelslegers <annelies.ketelslegers@student.kuleuven.be> 
 */
public abstract class Border {
	
	/**
	 * Initialize this new border attached to the given squares 
	 * in the given direction.
	 * 
	 * @param	square1
	 * 			The first square to which this new border must be attached.
	 * @param	square2
	 * 			The second square to which this new border must be attached.
	 * @param	direction
	 * 			The direction in which this new border must be attached 
	 * 			to the first square.
	 * 				The second square will be attached in the opposite 
	 * 				direction.
	 * @post 	This new border will be initialized as not terminated.
	 * 			| ! new.isTerminated()
	 * @effect	This new border is attached to the given squares.
	 * 			| constructBorder(square1, direction)
	 * 			| constructBorder(square2, direction.getOpposite())
	 * @throws	IllegalArgumentException
	 * 			One of the given squares was non effective.
	 * 			| if(square1 == null || square2 == null)
	 */
	@Raw
	public Border(Square square1, Square square2, Direction direction) throws IllegalArgumentException{
		if(square1 == null || square2 == null)
			throw new IllegalArgumentException("One of the given squares was non effective.");
		isTerminated = false;
		constructBorder(square1, direction);
		constructBorder(square2, direction.getOpposite());
	}
	
	/**
	 * Initialize this new border attached to the given square 
	 * in the given direction.
	 * 
	 * @param	square
	 * 			The square to which this new border must be attached.
	 * @param 	direction
	 * 			The direction in which this new border must be 
	 * 			attached to the square.
	 * @post	This new border will be initialized as not terminated.
	 * 			| ! new.isTerminated()
	 * @effect 	This new border is attached to the given square.
	 * 			| constructBorder(square, direction)
	 * @throws	IllegalArgumentException
	 * 			The given square was non effective.
	 * 			| if(square == null)
	 */
	@Raw
	public Border(Square square, Direction direction){
		if(square == null)
			throw new IllegalArgumentException("The given square was non effective.");
		isTerminated = false;
		constructBorder(square,direction);
	}
	
	/**
	 * Terminate this border.
	 * 
	 * @post 	This border is terminated.
	 * 			| new.isTerminated()	 
	 * @post 	If this border was not already terminated , 
	 *			it will be set terminated.
	 *			|if(! isTerminated()){
	 *			|	then new.isTerminated = true;
	 * @post 	This border no longer references a square as 
	 *			one of the two squares it is attached to.
	 *			|new.getSquare1() == null && new.getSquare2() == null
	 * @effect	if (one of) the squares of this border is effective,
	 *			it will be detached.
	 *			|if(getSquare1() != null){
	 *			|	detach(getSquare1());
	 *			|if(getSquare2() != null){
	 *			|	detach(getSquare2())
	 */
	public void terminate(){
		if(! isTerminated()){
			this.isTerminated = true;
			if(getSquare1() != null)
				detach(getSquare1());
			if(getSquare2() != null)
				detach(getSquare2());
		}
	}
	
	/**
	 * Check whether this border is terminated.
	 */
	@Basic @Raw
	public boolean isTerminated(){
		return this.isTerminated;
	}
	
	/**
	 * Variable registering whether this border is terminated.
	 */
	private boolean isTerminated;
	
	/**
	 * Return the first square to which this border is attached.
	 */
	@Basic @Raw
	public Square getSquare1(){
		return square1;
	}
	
	/**
	 * Return the second square to which this border is attached.
	 */
	@Basic @Raw
	public Square getSquare2(){
		return square2;
	}
	
	/**
	 * Return the other square that is attached to this border.
	 * 
	 * @param 	square
	 * 			The square of which we want to get the other square 
	 * 			attached to it by means of this border.
	 * @return	The first square if the given square was the second square 
	 * 			attached to this border.
	 * 			|if (getSquare2().equals(square))
	 * 			|	then result == getSquare1()
	 * @return	The second square if the given square was the first square 
	 * 			attached to this border.
	 * 			|if (getSquare1().equals(square))
	 * 			|	then result == getSquare2()
	 * @throws 	IllegalArgumentException
	 * 			The given square was not effective.
	 * 			| square == null
	 * @throws 	IllegalArgumentException
	 * 			The given square was not attached to this border.
	 * 			| (square != getSquare1()) && (square != getSquare2())
	 */
	public Square getOtherSquare(Square square) {
		if(square == null)
			throw new IllegalArgumentException("The given square was not effective.");
		if(square != getSquare1()){
			if (square != getSquare2())
				throw new IllegalArgumentException("The given square was not attached to this border");
			return getSquare1();}
		return getSquare2();
	}
	
	/**
	 * Check whether this border can have the given square as one of 
	 * it squares.
	 * 
	 * @param 	square
	 * 			The square to check
	 * @return 	If this border is terminated, true if and only if the 
	 * 			given square is not effective.
	 * 			|if (this.isTerminated())
	 *			|	then result == (square == null)
	 * @return 	Otherwise, return true if and only if the square is 
	 * 			not effective or if the square is not terminated.
	 * 			| result == (square == null ||! square.isTerminated())
	 */
	public boolean canHaveAsSquare(Square square){
		if (this.isTerminated())
			return square == null;
		return  (square == null || ! square.isTerminated());
	}
	

	/**
	 * Check whether the given direction is a valid direction for 
	 * any border.
	 * 
	 * @param 	direction
	 * 			The direction to be checked.
	 * @return	True if and only if the given direction is effective.
	 * 			|result = (direction != null )
	 */
	public boolean isValidDirection(Direction direction){
		return (direction != null );
	}
	
	/**
	 * Check whether this border has proper squares attached to it.
	 * 
	 * @return 	True if and only if this border is attached to at least one effective square
	 * 			and either this border is not attached to a first square 
	 * 					or this border can have the first square to which it is attached as one of its squares 
	 * 							and that first square references this border as its border
	 * 			and either this border is not attached to a second square 
	 * 					or this border can have the second square to which it is attached as one of its squares 
	 * 							and that second square references this border as its border.
	 * 			False otherwise.
	 * 			|result == (getSquare1() != null || getSquare2() != null)
	 *			|			&& ((getSquare1() == null) 
	 *			|					|| ((canHaveAsSquare(getSquare1())) && (getSquare1().hasBorder(this)==true)))
	 *			|			&& ((getSquare2() == null) 
	 *			|					|| ((canHaveAsSquare(getSquare2())) && (getSquare2().hasBorder(this)==true)))
	 */
	public boolean hasProperSquares(){
		if(getSquare1() != null || getSquare2() != null) 
			if ((getSquare1() == null) || ((canHaveAsSquare(getSquare1())) && (getSquare1().hasBorder(this)==true)))
				if ((getSquare2() == null) || ((canHaveAsSquare(getSquare2())) && (getSquare2().hasBorder(this)==true)))
					return true;
		return false;
	}
	
	/**
	 * Attach this border to the given square.
	 * 
	 * @param 	square
	 * 			The square to which this border must be attached.
	 * @param 	direction
	 * 			The direction in which the attachment should be made.
	 * @post 	The given square has this border as its border in the 
	 * 			given direction.
	 * 			|(new square).getBorderInDirection(direction) == this
	 * @post 	The given square will be attached to this border.
	 * 			| new.hasAsSquare(square)
	 * @throws 	IllegalStateException
	 * 			This border was already terminated
	 * 			or this border already has two squares attached to it.
	 * 			| isTerminated()
	 * 			|	||((getsquare1() != null) && (getSquare2() != null))
	 * @throws	IllegalArgumentException
	 * 			The given direction is not a valid direction
	 * 			or the given square is not a valid square
	 * 			or the given square was already attached to this border
	 * 			or the square already has a border in the given direction
	 * 			or the squares are already attached by an other border
	 * 			or the squares are attached to this border in 
	 * 			different directions.
	 * 			| ! isValidDirection(direction)
	 * 			| || !canHaveAsSquare(square)
	 * 			| || hasAsSquare(square)
	 * 			| || square.getBorder(direction)!= null
	 * 			| || (((getSquare1() == null) && (getSquare2() != null) 
	 * 			|		&& ((square.hasAsNeighbor(getSquare1())) || (getSquare1().getDirectionOfBorder(this)!= direction.getOpposite()))
	 * 			| 	|| ((getSquare2() == null) && (getSquare1() != null) 
	 * 			|		&& ((square.hasAsNeighbor(getSquare2()))) || getSquare2().getDirectionOfBorder(this) != direction.getOpposite()))
	 */
	public void constructBorder(Square square, Direction direction) throws IllegalArgumentException, IllegalStateException {
		if(this.isTerminated())
			throw new IllegalStateException("This border was already terminated.");
		if(! isValidDirection(direction))
			throw new IllegalArgumentException("The given direction was not a valid direction.");
		if(! canHaveAsSquare(square))
			throw new IllegalArgumentException("The given square was not a valid square.");
		if( hasAsSquare(square))
			throw new IllegalArgumentException("The given square was already attached to this border");
		if(square.getBorder(direction)!= null)
			throw new IllegalArgumentException("The square already has a border in the given direction.");
		if(getSquare1() == null){
			if((getSquare2() != null) && (square.hasAsNeighbor(getSquare2())))
				throw new IllegalArgumentException("Squares are already attached by an other border.");
			if(getSquare2() != null && getSquare2().getDirectionOfBorder(this) != direction.getOpposite())
				throw new IllegalArgumentException("The direction in which this border is attached to its squares must be opposites.");
			this.setSquare1(square);}
		else {
			if(getSquare2() == null){
				if(square.hasAsNeighbor(getSquare1()))
					throw new IllegalArgumentException("Squares are already attached by an other border");
				if(getSquare1().getDirectionOfBorder(this)!= direction.getOpposite())
					throw new IllegalArgumentException("The direction in which this border is attached to its squares must be opposites.");
				this.setSquare2(square);}
			else throw new IllegalStateException("Border already has 2 squares attached.");}
		square.attachBorder(direction, this);
	}
	

		
	
	/**
	 * Set the given square as the first square of this border.
	 * 
	 * @param 	square	
	 * 			The square to which this border must be attached.
	 * @post	If the new square is ineffective and the first square 
	 * 				of this border is terminated,
	 * 			or if the given square is ineffective and this border 
	 * 				is terminated, 
	 * 			or if this border can have the given square as a square 
	 * 				and this border does not already have the given square 
	 * 				as one of its squares and there is no square attached 
	 * 				to this border as its first square yet,
	 * 			this border will have the given square as its first square.
	 * 			| if(((square == null && getSquare1().isTerminated())) 
	 * 			|	|| (canHaveAsSquare(square) && getSquare1() == null))
	 * 			|		then  new.getSquare1() == square
	 */
	private void setSquare1(Square square){
		if((square == null && (getSquare1().isTerminated() 
				|| this.isTerminated())) || 
				(canHaveAsSquare(square) && ! hasAsSquare(square) 
						&& getSquare1() == null))
			this.square1 = square;
	}
	
	/**
	 * Set the given square as the second square of this border.
	 * 
	 * @param 	square	
	 * 			The square to which this border must be attached.
	 * @post	If the new square is ineffective and the second square 
	 * 				of this border is terminated,
	 * 			or if the given square is ineffective and this border 
	 * 				is terminated, 
	 *			or if this border can have the given square as a square 
	 *				and this border does not already have the given square 
	 *				as one of its squares and there is no square attached 
	 *				to this border as its second square yet,
	 * 			this border will have the given square as its second square.
	 * 			| if(((square == null && getSquare1().isTerminated())) 
	 * 			|	|| (canHaveAsSquare(square) && getSquare2() == null))
	 * 			|		then  new.getSquare2() == square
	 */
	private void setSquare2(Square square){
		if((square == null && (getSquare2().isTerminated() 
				|| this.isTerminated())) 
				|| (canHaveAsSquare(square) && ! hasAsSquare(square) 
						&& getSquare2() == null))
			this.square2 = square;
	}
	
	/**
	 * Variable referencing the first square to which this border 
	 * is attached.
	 */
	private Square square1;
	
	/**
	 * Variable referencing the second square to which this border 
	 * is attached.
	 */
	private Square square2;
		
	/**
	 * Check whether this border is attached to the given square.
	 * 
	 * @param square
	 * 			The square to check.
	 * @return	True if and only if one of the squares attached to this border references the given square.
	 * 			| result == (square == getSquare1()) || (square == getSquare2())
	 */
	public boolean hasAsSquare(Square square){
		return (square == getSquare1()) || (square == getSquare2());
	}
	/**
	 * Detach the given square from this border.
	 * 
	 * @param 	square
	 * 			The square that must be detached.
	 * @pre		The given square must be effective.
	 * 			| square!= null
	 * @pre 	Either this border must be terminated or the given square must be terminated, otherwise it's impossible to detach.
	 * 			| square.isTerminated() || this.isTerminated()
	 * @pre		This border must be attached to the given square.
	 * 			| hasSquare(square)
	 * @pre		Each square must always be attached to 1 border.
	 * 			To detach a border this square must at least have 2 borders attached to it.
	 * 			| square.getNbBorders() >=2
	 * @post	This border is not attached to the given square if this square is terminated.
	 * 			| if(square.isTerminated())
	 * 			|		then (new.getSquare1() != square) && (new.getSquare2() != square)
	 * @effect	If there no longer are squares attached to this border, this border will be terminated.
	 * 			| if((getSquare1() == square) && (getSquare2() == null))
	 * 			| 		|| ((getSquare2() == square) && (getSquare1() == null))
	 * 			| 	then this.terminate()
	 */
	public void detach(Square square) {
		assert(square != null);
		assert(square.isTerminated() ||  this.isTerminated());
		assert(this.hasAsSquare(square));
		assert(square.isTerminated() || square.getNbBorders()>=2 );
		if(square == getSquare1()){
			setSquare1(null);
			square.removeBorder(this);
			if(getSquare2() == null)
				terminate();
		}
		if(square  == getSquare2()){
			setSquare2(null);
			square.removeBorder(this);
			if(getSquare1() == null)
				terminate();
		}
	}
	
	/**
	 * Replace this border with the given border.
	 * 
	 * @param 	newBorder
	 * 			The new border that must be attached 
	 * @post	This border will be terminated.
	 * 			|new.isTerminated()
	 * @post	The given border will be attached to the squares of this
	 * 			border in the direction in which this border was attached,
	 * 			if they are effective.
	 * 			|if(this.getSquare1() != null)
	 * 			| 	then this.getSquare1().getBorder(this.getSquare1().getDirectionOfBorder(this)) 
	 * 			|		== newBorder
	 * 			|if(this.getSquare2() != null)
	 * 			| 	then this.getSquare2().getBorder(this.getSquare2().getDirectionOfBorder(this)) 
	 * 			|		== newBorder
	 * @throws IllegalArgumentException
	 * 			The given border must be effective and may not be terminated.
	 * 			|newBorder == null || newBorder.isTerminated()
	 */
	public void replaceWith(Border newBorder){
		if(newBorder == null || newBorder.isTerminated())
			throw new IllegalArgumentException("Unvalid new Border");
		Square square1 = getSquare1();
		Square square2 = getSquare2();
		Direction direction = square1.getDirectionOfBorder(this);
		this.terminate();
		if(newBorder == null || newBorder.isTerminated())
			throw new IllegalArgumentException("Illegal border");
		if(square1 instanceof Rock){
			this.square1 = null;
			((Rock) square1).replaceBorderWith(this, newBorder);
		}
		else if(square1 != null)
			newBorder.constructBorder(square1, direction.getOpposite());
		if(square2 instanceof Rock){
			this.square2 = null;
			((Rock) square2).replaceBorderWith(this, newBorder);
		}
		else if(square2 != null)
				newBorder.constructBorder(square2, direction);
	}
	
	/**
	 * Return a textual representation of this border.
	 */
	public abstract String toString();
	
}